package com.neborosoft.FlyingDog.logic.search;

import com.neborosoft.FlyingDog.logic.database.content.identified.Album;
import com.neborosoft.FlyingDog.logic.database.content.identified.Artist;
import com.neborosoft.FlyingDog.logic.database.content.identified.Audio;
import com.neborosoft.FlyingDog.logic.search.network.LastFMSearchParams;
import com.neborosoft.FlyingDog.logic.search.network.LastFMSearcher;
import com.neborosoft.FlyingDog.logic.search.network.VkAudioSearchParams;
import com.neborosoft.FlyingDog.logic.search.network.VkSearcher;
import com.neborosoft.FlyingDog.logic.search.parsers.LastFmResultParser;
import com.neborosoft.FlyingDog.logic.search.parsers.VkResultParser;
import com.neborosoft.lib.API_8.Collections;
import com.neborosoft.lib.collections.cache.*;
import com.neborosoft.lib.collections.queue.PageLazyQueue;
import com.neborosoft.lib.handlers.ErrorListener;
import com.neborosoft.lib.strings.Strings;
import com.neborosoft.lib.utilities.Utilities;
import org.apache.commons.collections15.KeyValue;
import org.json.JSONException;

import java.io.IOException;
import java.util.List;
import java.util.Queue;

/**
 * Created with IntelliJ IDEA.
 * User: CM
 * Date: 02.01.13
 * Time: 2:52
 * To change this template use File | Settings | File Templates.
 */
public class InternetSearchEngine {
    private static final int AUDIO_URLS_COUNT = 200;

    private LastFMSearcher lastFMSearcher = new LastFMSearcher();
    private LastFmResultParser lastFmResultParser = new LastFmResultParser();
    private VkResultParser vkResultParser = new VkResultParser();
    private VkSearcher vkSearcher = new VkSearcher();
    private Cache memoryCache;
    private Cache discCache;
    private CacheCombination cache;

    private void combineCaches(){
        cache = new CacheCombination(memoryCache, discCache);
    }

    private <V> V getFromCache(KeyValue key){
        return (V)cache.get(key);
    }

    public InternetSearchEngine(Cache memoryCache, Cache discCache) {
        this.memoryCache = memoryCache;
        this.discCache = discCache;

        if(discCache == null){
            this.discCache = new EmptyCache();
        }

        if(memoryCache == null){
            this.memoryCache = new EmptyCache();
        }
    }

    public InternetSearchEngine(int memoryCacheSize, int discCacheSize, CacheDirectoryPathGenerator discCacheDir) {
        if(memoryCacheSize > 0){
            memoryCache = new ObjectLruCache(memoryCacheSize);
        } else {
            memoryCache = new EmptyCache();
        }

        if(discCacheSize > 0){
            discCache = new ObjectLruDiskCache(discCacheSize, discCacheDir);
        } else {
            discCache = new EmptyCache();
        }
    }

    public InternetSearchEngine(int memoryCacheSize){
        this(memoryCacheSize, -1, null);
    }

    public InternetSearchEngine(){
        this(-1);
    }

    private interface LastFmSearchResultProvider<T>{
        String search(String query, LastFMSearchParams searchParams) throws IOException;
        SearchResult<T> parse(String response) throws JSONException;
    }

    private interface ResultProvider<T>{
        String search(Object query) throws IOException;
        T parse(String response) throws JSONException;
    }


    private static class LastFmSearchResultParams<T>{
        LastFmSearchResultProvider<T> resultProvider;
        String methodName;
        String query;
        int maxCount;
        int page;
    }

    private static class ResultParams<T>{
        ResultProvider<T> resultProvider;
        String methodName;
        Object query;
    }

    private <T> SearchResult<T> getLastFmSearchResult(LastFmSearchResultParams<T> lastFmParams) throws IOException{
        LastFMSearchParams lastFMSearchParams = new LastFMSearchParams();
        lastFMSearchParams.limit = lastFmParams.maxCount;
        lastFMSearchParams.page = lastFmParams.page;

        try{
            String key = Strings.join('_', lastFmParams.methodName, lastFmParams.query,
                    lastFmParams.maxCount, lastFmParams.page);
            SearchResult<T> searchResult = CacheUtils.saveGet(cache, key);

            if(searchResult == null){
                String response = lastFmParams.resultProvider.search(lastFmParams.query, lastFMSearchParams);
                searchResult = lastFmParams.resultProvider.parse(response);
                cache.put(key, searchResult);
            }

            return searchResult;
        }
        catch (JSONException e){
            throw new InvalidResponseError(e.getMessage());
        }
    }

    private <T> T getResult(ResultParams<T> resultParams) {
        try{
            String key = Strings.join('_', resultParams.methodName, resultParams.query, resultParams.resultProvider);
            T searchResult = CacheUtils.saveGet(cache, key);

            if(searchResult == null){
                String response = resultParams.resultProvider.search(resultParams.query);
                searchResult = resultParams.resultProvider.parse(response);
                cache.put(key, searchResult);
            }

            return searchResult;
        }
        catch (Exception e){
            Utilities.logException(e);
            return null;
        }
    }

    private <T> List<T> getResultList(ResultParams<List<T>> resultParams) {
        List<T> list = getResult(resultParams);
        if(list == null){
            return Collections.emptyList();
        }

        return list;
    }

    public SearchResult<Album> getAlbumsByName(String query, int maxCount, int page) throws IOException {
        LastFmSearchResultParams<Album> lastFmParams = new LastFmSearchResultParams<Album>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "getAlbumsByName";
        lastFmParams.query = query;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Album>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.getAlbumsByName(query, searchParams);
            }

            @Override
            public SearchResult<Album> parse(String response) throws JSONException {
                return lastFmResultParser.parseAlbums(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public SearchResult<Album> getAlbumsOfArtist(String artistName, int maxCount, int page) throws IOException {
        LastFmSearchResultParams<Album> lastFmParams = new LastFmSearchResultParams<Album>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "getAlbumsOfArtist";
        lastFmParams.query = artistName;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Album>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.getAlbumsOfArtist(query, searchParams);
            }

            @Override
            public SearchResult<Album> parse(String response) throws JSONException {
                return lastFmResultParser.getAlbumsOfArtist(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    private void updateAlbumId(Album album) throws IOException, JSONException {
        String name = album.getName();
        String artistName = album.getArtistName();

        if(name == null || artistName == null){
            throw new NullPointerException();
        }

        String response = lastFMSearcher.getAlbumByNameAndArtistName(name, artistName);
        int id = lastFmResultParser.getAlbumId(response);
        album.setId(id);
    }

    public List<Audio> getSongsOfAlbum(final Album album) {
        Integer id = (Integer)album.getId();

        try {
            if(id == null){
                updateAlbumId(album);
                id = (Integer)album.getId();
                if(id == null){
                    return Collections.emptyList();
                }
            }

            ResultParams<List<Audio>> resultParams = new ResultParams<List<Audio>>();
            resultParams.query = id;
            resultParams.methodName = "getSongsOfAlbum";
            resultParams.resultProvider = new ResultProvider<List<Audio>>() {
                @Override
                public String search(Object id) throws IOException {
                    return lastFMSearcher.getTracksByAlbumId((Integer)id);
                }

                @Override
                public List<Audio> parse(String response) throws JSONException {
                    return lastFmResultParser.getSongsOfAlbum(response, album);
                }
            };

            return getResultList(resultParams);

        } catch (JSONException e) {
            Utilities.logException(e);
            return Collections.emptyList();
        } catch (IOException e) {
            Utilities.logException(e);
            return Collections.emptyList();
        }
    }

    public SearchResult<Audio> searchAudios(String query, int maxCount, int page) throws IOException {
        LastFmSearchResultParams<Audio> lastFmParams = new LastFmSearchResultParams<Audio>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "searchAudios";
        lastFmParams.query = query;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Audio>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.searchTracks(query, searchParams);
            }

            @Override
            public SearchResult<Audio> parse(String response) throws JSONException {
                return lastFmResultParser.parseTracks(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public SearchResult<Audio> getSongsOfArtist(String artistName, int maxCount, int page) throws IOException {
        LastFmSearchResultParams<Audio> lastFmParams = new LastFmSearchResultParams<Audio>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "getSongsOfArtist";
        lastFmParams.query = artistName;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Audio>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.getTracksOfArtist(query, searchParams);
            }

            @Override
            public SearchResult<Audio> parse(String response) throws JSONException {
                return lastFmResultParser.getSongsOfArtist(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public SearchResult<Audio> getSongsByTag(String tag, int maxCount, int page) throws IOException {
        LastFmSearchResultParams<Audio> lastFmParams = new LastFmSearchResultParams<Audio>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "getSongsByTag";
        lastFmParams.query = tag;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Audio>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.getTracksByTag(query, searchParams);
            }

            @Override
            public SearchResult<Audio> parse(String response) throws JSONException {
                return lastFmResultParser.getSongsByTag(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public SearchResult<Artist> getArtistsByTag(String tag, int maxCount, int page) throws IOException {
        LastFmSearchResultParams<Artist> lastFmParams = new LastFmSearchResultParams<Artist>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "getArtistsByTag";
        lastFmParams.query = tag;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Artist>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.getArtistsByTag(query, searchParams);
            }

            @Override
            public SearchResult<Artist> parse(String response) throws JSONException {
                return lastFmResultParser.getArtistsByTag(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public boolean fillAudioInfo(Audio audio){
        String name = audio.getName();
        String artistName = audio.getArtistName();

        if(name == null || artistName == null){
            return false;
        }

        try {
            String response = lastFMSearcher.getTrackInfoByNameAndArtistName(name, artistName);
            return lastFmResultParser.fillAudioInfo(response, audio);
        } catch (IOException e) {
            Utilities.logException(e);
            return false;
        }
    }

    public Iterable<String> getAudioUrls(final Audio audio) {
        String name = audio.getName();
        String artistName = audio.getArtistName();

        if (name == null || artistName == null) {
            return Collections.emptyList();
        }

        String query = artistName + " " + name;
        fillAudioInfo(audio);

        ResultParams<List<String>> resultParams = new ResultParams<List<String>>();
        resultParams.methodName = "getAudioUrls";
        resultParams.query = query;
        resultParams.resultProvider = new ResultProvider<List<String>>() {
            @Override
            public String search(Object query) throws IOException {
                VkAudioSearchParams searchParams = new VkAudioSearchParams();
                searchParams.setCount(AUDIO_URLS_COUNT);
                return vkSearcher.searchAudios(query.toString(), searchParams);
            }

            @Override
            public List<String> parse(String response) throws JSONException {
                return vkResultParser.getAudioUrls(response, audio);
            }
        };

        return getResultList(resultParams);
    }

    public SearchResult<Artist> searchArtists(String query, int maxCount, int page) throws IOException {
        LastFmSearchResultParams<Artist> lastFmParams = new LastFmSearchResultParams<Artist>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "searchArtists";
        lastFmParams.query = query;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Artist>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.searchArtists(query, searchParams);
            }

            @Override
            public SearchResult<Artist> parse(String response) throws JSONException {
                return lastFmResultParser.parseArtists(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public SearchResult<Audio> getSimilarTracks(Audio audio, int maxCount, int page) throws IOException {
        final String name = audio.getName();
        final String artistName = audio.getArtistName();
        if(artistName == null || name == null){
            throw new NullPointerException("artistName == null || name == null");
        }

        LastFmSearchResultParams<Audio> lastFmParams = new LastFmSearchResultParams<Audio>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "getSimilarTracks";
        lastFmParams.query = artistName + ' ' + name;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Audio>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.getSimilarTracks(name, artistName, searchParams);
            }

            @Override
            public SearchResult<Audio> parse(String response) throws JSONException {
                return lastFmResultParser.getSimilarTracks(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public Queue<Audio> getSimilarTracks(final Audio audio, final int countPerPage, final ErrorListener errorListener) {
        return new PageLazyQueue<Audio>() {
            boolean isLastPage = false;

            @Override
            protected List<Audio> loadData(int pageNumber) {
                if(isLastPage){
                    return null;
                }

                try {
                    SearchResult<Audio> result = getSimilarTracks(audio, countPerPage, pageNumber);
                    isLastPage = result.isLastPage;
                    return result.elements;

                } catch (IOException e) {
                    if(errorListener != null){
                        errorListener.onError(e);
                    }
                    return null;
                }
            }
        };
    }

    public Queue<Audio> getSimilarTracks(final Audio audio, final int countPerPage) {
        return getSimilarTracks(audio, countPerPage, null);
    }

    public SearchResult<Artist> getSimilarArtists(Artist artist, int maxCount, int page) throws IOException {
        String artistName = artist.getName();
        if(artistName == null){
            throw new NullPointerException("artistName == null");
        }

        LastFmSearchResultParams<Artist> lastFmParams = new LastFmSearchResultParams<Artist>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "getSimilarArtists";
        lastFmParams.query = artistName;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<Artist>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.getSimilarArtists(query, searchParams);
            }

            @Override
            public SearchResult<Artist> parse(String response) throws JSONException {
                return lastFmResultParser.getSimilarArtists(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public SearchResult<String> searchTags(String query, int maxCount, int page) throws IOException {
        LastFmSearchResultParams<String> lastFmParams = new LastFmSearchResultParams<String>();
        lastFmParams.maxCount = maxCount;
        lastFmParams.page = page;
        lastFmParams.methodName = "searchTags";
        lastFmParams.query = query;

        lastFmParams.resultProvider = new LastFmSearchResultProvider<String>() {
            @Override
            public String search(String query, LastFMSearchParams searchParams) throws IOException {
                return lastFMSearcher.searchTags(query, searchParams);
            }

            @Override
            public SearchResult<String> parse(String response) throws JSONException {
                return lastFmResultParser.searchTags(response);
            }
        };

        return getLastFmSearchResult(lastFmParams);
    }

    public SearchResult<String> searchTagsHandleErrors(String query, int maxCount, int page){
        try {
            return searchTags(query, maxCount, page);
        } catch (IOException e) {
            Utilities.logException(e);
            return SearchResult.empty();
        }
    }
}
